use super::{err, err_value, ECode, Resp};
use reqwest::header::HeaderMap;
use serde_json::{self, Value};
use std::{env, time::Duration};
use tracing::{instrument, warn};

pub struct Client {
    client: reqwest::Client,
}

impl Client {
    pub async fn cfgdb(&self, data: &Value, url: &str) -> Resp {
        let cmd = format!("cfgdb/{}", url);
        self.query(data, "9981", &cmd, None, true).await
    }

    pub async fn job(&self, data: &Value, url: &str) -> Resp {
        let cmd = format!("job/{}", url);
        self.query(data, "9987", &cmd, None, true).await
    }

    pub async fn real(&self, data: &Value, url: &str) -> Resp {
        let cmd = format!("real/{}", url);
        self.query(data, "9984", &cmd, None, true).await
    }

    pub async fn timedb(&self, data: &Value, url: &str) -> Resp {
        let cmd = format!("timedb/{}", url);
        self.query(data, "9983", &cmd, None, true).await
    }

    pub async fn timedb_nolog(&self, data: &Value, url: &str) -> Resp {
        let cmd = format!("timedb/{}", url);
        self.query(data, "9983", &cmd, None, false).await
    }

    pub async fn plc(&self, data: &Value, url: &str) -> Resp {
        let cmd = format!("plc/{}", url);
        self.query(data, "9985", &cmd, None, true).await
    }

    pub async fn timedb_ip(&self, data: &Value, url: &str, ip: &str) -> Resp {
        let cmd = format!("timedb/{}", url);
        self.query(data, "9983", &cmd, Some(ip), true).await
    }

    pub async fn netplc(&self, data: &Value, url: &str) -> Resp {
        let cmd = format!("netplc/{}", url);
        self.query(data, "9960", &cmd, None, true).await
    }

    #[instrument(skip_all, name = "query")]
    async fn query(
        &self,
        data: &Value,
        port: &str,
        path: &str,
        ip: Option<&str>,
        log: bool,
    ) -> Resp {
        let url = self.get_url(port, path, ip);

        let resp = self
            .client
            .post(&url)
            .body(data.to_string())
            .send()
            .await
            .or_else(|e| {
                warn!("query post error, {url}: {:?}", e);
                err!(ECode::Post, e)
            })?;

        let code = resp.status().as_u16();

        let cmd = resp.text().await.or_else(|e| {
            warn!("query text error, {url}: {:?}", e);
            err!(ECode::PostText, e)
        })?;

        let body: Value = serde_json::from_str(&cmd).or_else(|e| {
            warn!("query json error, {url}: {:?}, cmd: {:?}", e, &cmd);
            err!(ECode::PostJson, e)
        })?;

        if code == warp::http::StatusCode::EXPECTATION_FAILED {
            if log {
                warn!("send http json error, {url}: {:?}, cmd: {:?}", body, &data);
            }
            err!(ECode::Post, body)
        } else {
            Ok(body)
        }
    }

    fn get_url(&self, port: &str, url: &str, addr: Option<&str>) -> String {
        if let Some(ip) = addr {
            return format!("http://{}/{}", ip, url);
        }

        if cfg!(windows) {
            format!("http://127.0.0.1:{}/{}", port, url)
        } else {
            match env::var("RUN_IN_LOCAL") {
                Ok(_) => format!(r#"http://localhost:{}/{}"#, port, url),
                Err(_) => format!(r#"http://dockerhost/{}"#, url),
            }
        }
    }

    #[instrument(skip_all, name = "send_http")]
    pub async fn send_http(&self, data: &str, url: &str, timeout: i64, head: HeaderMap) -> Resp {
        let duration = Duration::from_millis(timeout as u64);

        warn!(
            "send_http, url: {}, head: {:?}, cmd: {:?}",
            &url, &head, data
        );

        let resp = self
            .client
            .post(url)
            .headers(head)
            .timeout(duration)
            .body(data.to_string())
            .send()
            .await
            .or_else(|e| {
                warn!("send http post error, {url}: {:?}", e);
                err!(ECode::Post, e)
            })?;

        warn!("send_http return : {}, ret: {:?}", &url, &resp);

        let cmd = resp.text().await.or_else(|e| {
            warn!("send http text error, {url}: {:?}", e);
            err!(ECode::PostText, e)
        })?;

        let body: Value = serde_json::from_str(&cmd).or_else(|e| {
            warn!("send http json error, {url}: {:?}, cmd: {:?}", e, &cmd);
            err!(ECode::PostJson, e)
        })?;

        Ok(body)
    }

    pub fn new() -> Self {
        Client {
            client: reqwest::Client::new(),
        }
    }
}
